#include <dlfcn.h>
#include <string.h>

#include "redmapobject.h"
#include "redcodeobject.h"
#include "redfileobject.h"
#include "redframeobject.h"
#include "redmoduleobject.h"
#include "redstringobject.h"
#include "redexceptionobject.h"

#include "gc.h"
#include "errors.h"
#include "parser.h"
#include "rstring.h"
#include "generator.h"
#include "tokenizer.h"
#include "parser_unref.h"

static void module_traverse(RedObject *self, tp_visit_fn_t visit, void *args)
{
	Red_VISIT(self->ob_attrs);
}

RedTypeObject RedType_Module =
{
	/* modules are mutable */
	RedType_Mutable_INIT("module", RedModuleObject, RedType_Object)
	
	.tp_traverse = module_traverse,
};

static RedObject *import_binary(const char *fname, RedObject *name)
{
	/* load dynamic library into memory */
	void *lib = dlopen(fname, RTLD_LAZY);

	/* library not found */
	if (!lib)
	{
		Red_UNREF(name);
		RedException_ThrowErrno(&RedExc_ImportError);
		return NULL;
	}

	/* lookup initialize routine `module_entry` */
	RedModuleEntry entry = dlsym(lib, "module_entry");

	/* not a valid extension module */
	if (!entry)
	{
		Red_UNREF(name);
		RedException_ThrowFormat(&RedExc_ImportError, "C extensions must export entry function 'module_entry'");
		return NULL;
	}

	/* create module object */
	RedObject *result = RedObject_Create(&RedType_Module, NULL);

	if (!result)
	{
		Red_UNREF(name);
		return NULL;
	}

	/* execute module initialization routine */
	if (!entry(result, name))
	{
		Red_UNREF(name);
		Red_UNREF(result);
		RedException_ThrowFormat(&RedExc_ImportError, "Failed to initialize C extension module '%s'", fname);
		return NULL;
	}

	Red_UNREF(name);
	RedObject_GC_Insert(result);
	RedObject_GC_Insert(result->ob_attrs);
	return result;
}

static RedObject *import_script(const char *fname, RedObject *name, RedObject *consts)
{
	/* open script file */
	RedObject *fp = RedFile_Open(fname, "r");

	if (!fp)
	{
		Red_UNREF(name);
		return NULL;
	}

	/* read all scripts */
	RedObject *script = RedFile_ReadAll(fp);

	if (!script)
	{
		Red_UNREF(fp);
		Red_UNREF(name);
		return NULL;
	}

	/* compile source code */
	RedObject *code = RedModule_Compile(fname, name, script, consts);

	/* close script file after compile */
	Red_UNREF(fp);
	Red_UNREF(name);
	Red_UNREF(script);

	if (!code)
		return NULL;

	/* eval this script into a module object */
	RedObject *module = RedModule_Eval(code);

	Red_UNREF(code);
	return module;
}

RedObject *RedModule_Eval(RedObject *code)
{
	/* create module object */
	RedObject *result = RedObject_Create(&RedType_Module, NULL);

	if (!result)
		return NULL;

	/* initialize module object using self->ob_attrs */
	RedObject *frame = RedFrame_Acquire(code, result->ob_attrs, NULL);

	if (!frame)
	{
		Red_UNREF(result);
		return NULL;
	}

	/* eval this frame in eval_context */
	RedObject *eval = RedEval_ExecFrame(frame);

	/* eval failed, release module and throw an error */
	if (!eval)
	{
		Red_UNREF(frame);
		Red_UNREF(result);
		return NULL;
	}

	/* add this module into GC track list */
	Red_UNREF(eval);
	Red_UNREF(frame);
	RedObject_GC_Insert(result);
	RedObject_GC_Insert(result->ob_attrs);
	return result;
}

RedObject *RedModule_Import(const char *fname, RedObject *globals)
{
	/* extract module name and module type */
	const char *extname = strrchr(fname, '.');
	const char *basename = strrchr(fname, '/');

	/* no extension found */
	if (!extname)
	{
		RedException_ThrowFormat(&RedExc_ImportError, "Module file must have ext-name");
		return NULL;
	}

	/* no basename */
	if (basename == extname)
	{
		RedException_ThrowFormat(&RedExc_ImportError, "Module file must have file name");
		return NULL;
	}

	/* process relative path situation */
	if (basename)
		basename++;
	else
		basename = fname;

	/* extract it's file name as module name */
	RedObject *modname = RedString_FromBufferAndSize(basename, (int)(extname - basename));

	if (!modname)
		return NULL;

	/* compiled extension module */
	if (!strcasecmp(extname, ".so") ||		/* Linux */
		!strcasecmp(extname, ".dylib") ||	/* OS X */
		!strcasecmp(extname, ".redmod"))	/* Windows */
		return import_binary(fname, modname);

	/* script module */
	if (!strcasecmp(extname, ".rs"))
		return import_script(fname, modname, globals);

	Red_UNREF(modname);
	RedException_ThrowFormat(&RedExc_ImportError, "Unrecognized file extension '%s'", extname);
	return NULL;
}

RedObject *RedModule_Compile(const char *fname, RedObject *name, RedObject *script, RedObject *consts)
{
	/* initialize source manually */
	rstring_t source;
	source.ref = -1;
	source.length = RedString_Length(script);
	source.content = RedString_Buffer(script);

	/* initialize tokenizer */
	tokenizer_t tokenizer;
	tokenizer_init(&tokenizer);
	tokenizer_set_source(&tokenizer, &source);

	/* initialize parser */
	parser_t parser;
	parser_init(&parser, consts, &tokenizer);

	/* parse source */
	compond_t *ast = parser_parse(&parser);

	/* compilation successful */
	if (ast)
	{
		/* generate bytecodes */
		rstring_t *fn = rstring_new_string(fname);
		RedObject *code = generator_generate(ast, fn, &source, name, consts);

		parser_free(&parser);
		rstring_release(&fn);
		RedObject_GC_Insert(code);
		return code;
	}

	/* errors occured, convert the compiler errors to RedException */
	/* not a tokenizer error, just pass the errors_message as exception message */
	if (errors_code() != e_tokenizer)
	{
		parser_free(&parser);
		RedException_ThrowFormat(&RedExc_CompileError, "SemanticError at line %d, character %d: %s", tokenizer.y, tokenizer.x, errors_message());
		return NULL;
	}

	/* avoid the line-too-long warning */
	const char *format;

	/* tokenizer errors don't have messages */
	switch (tokenizer.error_code)
	{
		case tke_ok:
			format = "SyntaxError at line %d, character %d: source incomplete";
			break;

		case tke_eof:
			format = "SyntaxError at line %d, character %d: EOF when scanning strings";
			break;

		case tke_inv_op:
			format = "SyntaxError at line %d, character %d: invalid operator '%c'";
			break;

		case tke_inv_char:
			format = "SyntaxError at line %d, character %d: invalid character '%c'";
			break;

		case tke_inv_float:
			format = "SyntaxError at line %d, character %d: invalid float point format";
			break;

		default:
			format = "SyntaxError at line %d, character %d: unknown internal error";
			break;
	}

	parser_free(&parser);
	RedException_ThrowFormat(&RedExc_CompileError, format, tokenizer.y, tokenizer.x, tokenizer.error_char);
	return NULL;
}
